package com.gking.centerSystem.utils;


import com.alibaba.fastjson.JSON;
import com.gking.centerSystem.common.Result;
import com.gking.centerSystem.entity.AccessIpList;
import com.gking.centerSystem.service.AccessIpList.AccessIpListService;
import com.gking.centerSystem.service.AccessRejectionCollect.AccessRejectionCollectService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;


/**
 * 访问策略工具类
 * 黑名单策略 、 白名单策略 、 灰名单策略 、 频率访问策略
 */
@Slf4j
@Component
public class AccessPolicyUtil {

    @Resource
    private StringRedisTemplate stringRedisTemplate0;
    private static StringRedisTemplate stringRedisTemplate;

    @Autowired
    private AccessRejectionCollectService accessRejectionCollectService0;
    private static AccessRejectionCollectService accessRejectionCollectService;

    @Autowired
    private AccessIpListService accessIpListService0;
    private static AccessIpListService accessIpListService;

    private static final String System_Black_IP = "System:Access:Black:IP"; // 黑名单 IP 列表
    private static final String System_White_IP = "System:Access:White:IP"; //  白名单 IP 列表

    private static final String System_Grey_IP = "System:Access:Grey:IP_"; // 某个灰名单 ip
    private static final Integer Rejection_Max_Count = 50; // 允许某个时间段最大接受的受控次数
    private static final Integer Rejection_Max_Count_Total = Rejection_Max_Count * 8; // 允许某天最大接受的受控次数

    private static final Long Unit_Time = 3L; // 单位时间
    private static final Long Limit_Unit_Time = 5L; // 限流单位时间
    private static final String IP_Access_Frequency = "IP:Access:Frequency:IP_"; // 某个ip单位时间访问频率
    private static final Integer Second_Max_Handle_Count = 20; // 允许每秒最大处理某个IP发送过来的请求数量
    private static final String IP_Frequency_Top_Count = "IP:Frequency:Top:Count:IP_"; // 某个ip单位时间访问达到频率顶峰次数
    private static final Integer IP_Frequency_Top_Max_Count = 3; //允许接受某个ip在一个单位时间段达到访问频率顶峰的最大次数


    /**
     * 没有这个方法会报空指针，因为还没有注入完成就被注册到 spring 容器中了，
     * 所以这里用一个局部变量保存注入内容，直接使用局部变量
     * <p>
     * 执行顺序： construct>>PostConstruct>>static，
     * 所以在 static 中使用的对象都是在 PostConstruct 里提前组装好的
     */
    @PostConstruct
    private void init() {
        stringRedisTemplate = this.stringRedisTemplate0;
        accessIpListService = this.accessIpListService0;
        accessRejectionCollectService = this.accessRejectionCollectService0;
    }


    private void BlackPolicy(String clientIp) {
        // 判断是否已经达到了某天的最大受控次数，
        // 达到了则判定为恶意IP，加入黑名单，禁止访问
        Integer dateRejectionCollect = accessRejectionCollectService.getDateRejectionCollect(clientIp);
        if (dateRejectionCollect > Rejection_Max_Count_Total) {
            AccessIpList ac = new AccessIpList();
            ac.setIpAddress(clientIp);
            ac.setType("Black");
            ac.setCreateUserId("System");
            Result<String> result = accessIpListService.addAccessIpList(ac);
            if (result.getCode() != 200) {
                log.info("ip 已存在受控ip列表中，无法再次添加");
                return;
            }

            List<String> blackIpList;
            String blackIpListStr = stringRedisTemplate.opsForValue().get(System_Black_IP);
            if (blackIpListStr != null) {
                blackIpList = JSON.parseArray(blackIpListStr, String.class);
                blackIpList.add(clientIp);
                stringRedisTemplate.opsForValue().set(System_Black_IP,
                        JSON.toJSONString(blackIpList), 1, TimeUnit.HOURS);
            }
        }
    }

    private void GreyPolicy(String clientIp) {
        // 判断是否已经达到了某个时间段的最大受控次数，
        // 达到了则暂时放入灰名单中，限制访问
        Integer datePeriodRejectionCollect = accessRejectionCollectService.getDatePeriodRejectionCollect(clientIp);
        if (datePeriodRejectionCollect > Rejection_Max_Count) {
            stringRedisTemplate.opsForValue().set(System_Grey_IP + clientIp,
                    "1", 1, TimeUnit.HOURS);
        }
    }

    /**
     * 判断ip是否需要受控，并记录
     */
    @Transactional
    public void judgeIpControlled(Result result, HttpServletRequest request) {
        Integer code = result.getCode();
        if (code == 200) return;

        String clientIp = GetAuthInfoUtil.getClientIp(request);
        try {
            // 添加受控记录
            accessRejectionCollectService.setRejectionCollect(clientIp);

            this.GreyPolicy(clientIp);
            this.BlackPolicy(clientIp);
        } catch (Exception e) {
            log.error("访问策略工具类 判断ip 是否需要受控 异常", e);
        }
    }

    /**
     * 清除redis中的缓存名单
     *
     * @param type 清除类型 Black White All
     */
    public static void resetRedisData(String type) {
        if (type.equals("Black") || type.equals("All"))
            stringRedisTemplate.delete(System_Black_IP);
        if (type.equals("White") || type.equals("All"))
            stringRedisTemplate.delete(System_White_IP);
    }

    /**
     * 黑名单策略，校验通过，则返回true，未通过返回false
     *
     * @param request 请求
     * @return b
     */
    public Boolean BlackPolicy(HttpServletRequest request) {
        // 获取数据库或者redis缓存中的黑名单
        List<String> blackIpList = new ArrayList<>();
        String blackIpListStr = stringRedisTemplate.opsForValue().get(System_Black_IP);

        if (blackIpListStr == null) {
            Result<List<AccessIpList>> black = accessIpListService.getAllIpByType("Black");
            for (AccessIpList ac : black.getData()) blackIpList.add(ac.getIpAddress());

            stringRedisTemplate.opsForValue().set(System_Black_IP,
                    JSON.toJSONString(blackIpList), 1, TimeUnit.HOURS);
        } else {
            blackIpList = JSON.parseArray(blackIpListStr, String.class);
        }

        // 判断是否存在黑名单中
        String clientIp = GetAuthInfoUtil.getClientIp(request);
        boolean contains = blackIpList.contains(clientIp);

        // 存在则返回 false
        return !contains;
    }

    /**
     * 白名单策略，校验通过，则返回true，未通过返回false
     */
    public Boolean WhitePolicy(HttpServletRequest request) {
        // 获取数据库或者redis缓存中的白名单
        List<String> whiteIpList = new ArrayList<>();
        String whiteIpListStr = stringRedisTemplate.opsForValue().get(System_White_IP);

        if (whiteIpListStr == null) {
            Result<List<AccessIpList>> white = accessIpListService.getAllIpByType("White");
            for (AccessIpList ac : white.getData()) whiteIpList.add(ac.getIpAddress());

            stringRedisTemplate.opsForValue().set(System_White_IP,
                    JSON.toJSONString(whiteIpList), 1, TimeUnit.HOURS);
        } else {
            whiteIpList = JSON.parseArray(whiteIpListStr, String.class);
        }

        // 判断是否存在白名单中
        // 存在则返回 true
        String clientIp = GetAuthInfoUtil.getClientIp(request);
        return whiteIpList.contains(clientIp);
    }

    /**
     * 灰名单策略，校验通过，则返回true，未通过返回false
     */
    public Boolean GreyPolicy(HttpServletRequest request) {
        String clientIp = GetAuthInfoUtil.getClientIp(request);
        String s = stringRedisTemplate.opsForValue().get(System_Grey_IP + clientIp);
        if (s == null) return true;
        else return false;
    }

    /**
     * 频率访问策略，校验通过，则返回true，未通过返回false
     * 超过每秒设定的最大请求数，则校验不通过
     */
    // TODO 重构访问策略
    public Boolean FrequencyAccessPolicy(HttpServletRequest request) {
        String clientIp = GetAuthInfoUtil.getClientIp(request);
        String accessFrequencyKey = IP_Access_Frequency + clientIp;
        String frequencyTopCountKey = IP_Frequency_Top_Count + clientIp;
        int secondMaxHandleCount = Second_Max_Handle_Count;


        // 当获取到ip每秒访问频率为空时，重设ip每秒访问频率
        String count = stringRedisTemplate.opsForValue().get(accessFrequencyKey);
        int time = 1; //频率单位时间 1s， 测试时调大一点
        if (count == null) {
            stringRedisTemplate.opsForValue().set(accessFrequencyKey, "1", time, TimeUnit.SECONDS);
            return true;
        }

        // 更新当前ip每秒访问访问频率
        int secondTimeFrequency = Integer.parseInt(count) + 1;
        // 获取当前键，过期时间
        Long ttl = stringRedisTemplate.execute((RedisCallback<Long>) redisConnection -> redisConnection.ttl(accessFrequencyKey.getBytes()));
        // 更新ip每秒访问访问频率
        stringRedisTemplate.execute((RedisCallback<Object>) (redisConnection) -> {
            // 乐观锁，监听一个键值的变化情况，如果在事务执行前这些键被其他客户端修改，事务会被中止。
            // 这种方式可以用来避免并发写操作引起的冲突。
            // redisConnection.watch(key.getBytes());

            redisConnection.multi(); // 开始事务

            // 修改键，的值
            stringRedisTemplate.opsForValue().set(accessFrequencyKey, Integer.toString(secondTimeFrequency), time, TimeUnit.SECONDS);
            // 如果键有过期时间，则重新设置原来的过期时间
            if (ttl != null && ttl > 0) redisConnection.expire(accessFrequencyKey.getBytes(), ttl);

            redisConnection.exec(); //执行事务
            // List<Object> results = redisConnection.exec();

            return null;
        });


        // 1.1 获取ip单位时间访问频率达到顶峰的次数，
        String frequencyTopCount = stringRedisTemplate.opsForValue().get(frequencyTopCountKey);
        int resetRequestHandleCount = 10; // 重新设置的每秒最大处理某个IP发送过来的请求数量
        // 1.2 当次数不为空时，
        if (frequencyTopCount != null) {
            // 1.3 判断是否要限流，限流的话，重设单位时间处理请求次数，每秒最多处理10个请求
            int i = Integer.parseInt(frequencyTopCount);
            if (i > IP_Frequency_Top_Max_Count) secondMaxHandleCount = resetRequestHandleCount;
        }

        // 判断当前ip每秒访问频率是否达到顶峰，没有的话返回 true ，
        // 有的话进行限流信息设置，返回 false
        if (secondTimeFrequency > secondMaxHandleCount) {
            // 当secondMaxHandleCount = resetRequestHandleCount 时，表示已经达到顶峰，已经进行了限流，
            // 此时不再进行限流信息设置，直接返回false
            if (secondMaxHandleCount == resetRequestHandleCount) return false;

            if (frequencyTopCount == null) {
                // 1.4 当次数为空时，重设ip单位时间访问频率达到顶峰的次数
                stringRedisTemplate.opsForValue().set(frequencyTopCountKey, "1", Unit_Time, TimeUnit.SECONDS);
            } else {
                // 1.5 当次数不为空时，更新ip单位时间访问频率达到顶峰的次数，
                // 1.6 如果ip单位时间访问频率达到顶峰的次数超过了限制次数，则进行单位时间限流
                int i = Integer.parseInt(frequencyTopCount) + 1;
                if (i > IP_Frequency_Top_Max_Count) {
                    stringRedisTemplate.opsForValue().set(frequencyTopCountKey, Integer.toString(i), Limit_Unit_Time, TimeUnit.MINUTES);
                } else {
                    Long expire = stringRedisTemplate.getExpire(frequencyTopCountKey);
                    if (expire == null) expire = Unit_Time;
                    stringRedisTemplate.opsForValue().set(frequencyTopCountKey, Integer.toString(i), expire, TimeUnit.SECONDS);
                }
            }

            return false;
        }

        return true;
    }

}
